AstVerb.php

<?php

namespace Tlf\Scrawl\Ext\MdVerb;

class Ast extends \Tlf\Scrawl\DoNothingExtension {

    public \Tlf\Scrawl $scrawl;

    public array $status = [
        'keys_found'=>[],
        'keys_missed'=>[],
        'filters_found'=>[],
        'filters_missed'=>[],
        'templates_found'=>[],
        'templates_missed'=>[],
    ];

    public function __construct($scrawl){
        $this->scrawl = $scrawl;
    }

    /**
     * Print information about a parsed PHP class. Use template `ast/debug` to get information about the AST path. (*Note: The templates are not very good, but you can create your own*)
     *
     * @usage `@ast(ast_path, template_name, filters)`, like `@ast(class[ClassName].methods[MethodName].docblock[Description], ast/default, filter1.filter2.filter3)`. See available [filters](/docs/api/src/Utility/Filters.php.md) & [templates](/docs/ASTTemplates.md)
     * @output the template's string output or the value pointed to by the `ast_path` if a template is not used and the value is a string.
     *
     * @note `@ast()` can accept a dot-path like `class.ClassName.methods.MethodName`, but the array-access version is recommended.
     */
    public function get_markdown(string $key, string $template='ast/default', ?string $filters=null): string {

        $this->scrawl->info('@ast()', $key);
        $value = $this->get_ast($key);
        $template_out = $this->scrawl->get_template($template, [$key, $value, $this], $did_find_template);

        if ($did_find_template===true){
            $this->status['templates_found'][$template] = $template;
        } else {
            $this->status['templates_missed'][$template] = $template;
        }

        if ($filters!=null){
            $template_out = $this->apply_filters($template_out, $filters);
        }

        return $template_out;
    }

    /**
     * 
     */
    protected function apply_filters(string $input, string $filters_list): string {
        $filters = explode(".", $filters_list);
        $out = $input;
        foreach ($filters as $f){
            if (method_exists(\Tlf\Scrawl\Utility\Filters::class, $f)){
                $this->status['filters_found'][$f] = $f;
                $out = \Tlf\Scrawl\Utility\Filters::$f($out);
            } else {
                $this->status['filters_missed'][$f] = $f;
                $this->scrawl->warn("Filter Not Found", "'$f' is not a valid filter.");
            }
        }
        return $out;
    }

    /**
     * @param $key a dotproperty like `class.ClassName.methods.MethodName.docblock.description`
     * @param $length the number of dots in the dotproperty to traverse. -1 means all
     */
    public function get_ast(string $key, int $length=-1){
        $dot_parts = explode('.',$key);

        $parts = [];
        foreach($dot_parts as $p){
            if (substr($p,-1)==']'){
                $pos = strpos($p, '[');
                if ($pos===false)$parts[] = $p;
                else {
                    $parts[] = substr($p,0,$pos);
                    $parts[] = substr($p, $pos+1, -1);
                }
            } else {
                $parts[] = $p;
            }
        }

        if ($length!=-1){
            $parts = array_slice($parts, 0,$length);
        }
        $group = array_shift($parts);
        if ($group == 'file'){
            $file = implode(".", $parts);
            $ast = $this->scrawl->get_ast($file);
            if (is_array($ast)){
                $ast['relPath'] = $file;
            } else {
                $this->status['keys_missed'][$key] = $key;
                $this->scrawl->warn("File '$file' not found", "Can't load file ast");
                return null;
            }

            $this->status['keys_found'][$key] = $key;
            return $ast;
        } else {
            $class = array_shift($parts);
            $ast = $this->scrawl->get('ast', $group.'.'.$class);

            if ($ast==null){
                $this->status['keys_missed'][$key] = $key;
                $this->scrawl->warn("Ast $group not found", "Can't load '$class'.");
                return;
            }


            // echo "\n\nAST:";
            // var_dump($ast);
            // var_dump($group);
            // var_dump($class);
            // echo "\n\n";

            $stack = $group.'.'.$class;
            $next = $ast;
            foreach ($parts as $p){
                $current = $next;
                $stack .= '.'.$p;

                // echo "\n\nStack: $stack";
                if (!isset($current[$p])&&is_array($current)){
                    foreach ($current as $i=>$item){
                        if (!is_numeric($i))continue;
                        //echo "\n\n".$p."::";
                        //echo $item['name']."\n\n";
                        if ($item['name']==$p){
                            $next = $item;
                            continue 2;
                        }
                    }
                    $this->scrawl->warn("Ast Not Found", "Can't load '$key'. Failed at '$current.$p'");
                    $this->status['keys_missed'][$key] = $key;
                    return null;
                } else if (!isset($current[$p])){
                    $this->scrawl->warn("Ast Not Found", "Can't load '$key'. Failed at '$p'");
                    $this->status['keys_missed'][$key] = $key;
                    return null;
                }

                $next = $current[$p];
            }

            $this->status['keys_found'][$key] = $key;
            return $next;
        }

        $this->status['keys_missed'][$key] = $key;

    }

    public function getVerbs(): array{
        return [
            'ast'=>'verbAst', //alias for @ast_class()
            'classMethods'=>'getClassMethodsTemplate',
            'ast_class'=> 'getAstClassInfo',
        ];
    }

    /**
     *
     * @param $fqn The fully qualified name, like a class name or function with its namespace
     * @param $dotProperty For class, something like 'methods.methodName.docblock' to get a docblock for the given class. 
     *
     * @example @ast(\Tlf\Scrawl\Ext\MdVerb\Ast, methods.getAstClassInfo.docblock)
     * @mdverb ast
     *
     */
    public function getAstClassInfo(array $info, string $fqn, string $dotProperty){
        // @ast(class,Phad\Test\Documentation,methods.testWriteAView.docblock)
        
        $class = $this->scrawl->getOutput('astClass', $fqn);

        // var_dump(array_keys($this->scrawl->getOutputs('astClass')));

        if ($class == 'null') return "class '$fqn' not found in ast.";
    


        $propStack = explode('.', $dotProperty);

        $head = $class;
        if (!is_array($head)){
            $file = $info['file']->path;
            // $this->scrawl->error('@ast or @ast_class in '.$file,'requires "astClass" output for fqn "'.$fqn.'" to be an array, but a '. gettype($class).' was returned.');
            $this->scrawl->error("@ast($fqn, $dotProperty) failed", 'in '.$file);
            return "@ast($fqn) failed";
        }
        foreach ($propStack as $prop){
            if ($prop=='*'){
                return print_r($head,true);
            }
            
            if (!isset($head[$prop])){
                $options =  [];
                foreach ($head as $key=>$value){
                    if (is_numeric($key) && ($value['name']??null)==$prop){
                        $head = $head[$key];
                        continue 2;
                    } else if (is_numeric($key) && isset($value['name'])){
                        $options[] = $value['name'];
                    }
                }
                $options = array_merge($options, array_keys($head));
                $msg = "Cannot find '$prop' part of '$dotProperty' on '$fqn'. You may try one of: ". implode(", ", $options);
                $this->scrawl->error('@ast or @ast_class', $msg);
                return $msg;
            }
            $head = $head[$prop];
        }

        if (is_array($head)){
            if (isset($head['body']))return $head['body'];
            else if (isset($head['description']))return $head['description'];
            else if (isset($head['src']))return $head['src'];
            $msg="Found an array for '$dotProperty' on '$fqn' with keys: ".implode(", ", array_keys($head));
            $this->scrawl->error('@ast or @ast_class', $msg);
            return $msg;
        }

        return $head;
    }

    /**
     *
     * @return string replacement
     */
    public function verbAst($info, $className, $dotProperty){

        //
        // This method not currently functional. Ugggh!!!
        //

        // $parts = explode('.', $classDotThing);
        // $class = array_shift($parts);
        // return $this->getAstClassInfo($info, $class, 'method.'.implode('.',$parts));
        return $this->getAstClassInfo($info, $className, $dotProperty);
        // $key = $class;
        // var_dump($key);
        // $output = $this->scrawl->getOutput('api',$key);
//
        // if (trim($output)=='')return "--ast '${class}' not found--";
//
        // return $output;
    }

    public function getClassMethodsTemplate($verb, $argListStr, $line){
        if ($verb!='classMethods')return;

        $args = explode(',', $argListStr);

        $className = $args[0];
        $visibility = '*';
        if (isset($args[1])){
            $visibility = trim($args[1]);
        }
        
        $class = $this->scrawl->getOutput('astClass', $className);

        $template = dirname(__DIR__,2).'/Template/classMethods.md.php';
        ob_start();
        require($template);
        $final = ob_get_clean();
        return $final;
    }



    public function scrawl_finished(){

        $this->scrawl->header("@ast() summary");

        $this->scrawl->good("ASTs found", count($this->status['keys_found']));
        if (count($this->status['keys_missed'])==0){
            $this->scrawl->info("All ASTs found");
        } else {
            $this->scrawl->warn("ASTs missed", implode(", ", $this->status['keys_missed']));
        }

        $this->scrawl->good("Templates found", count($this->status['templates_found']));
        if (count($this->status['templates_missed'])==0){
            $this->scrawl->info("All templates found");
        } else {
            $this->scrawl->warn("Templates missed", implode(", ", $this->status['templates_missed']));
        }

        $this->scrawl->good("Filters found", count($this->status['filters_found']));
        if (count($this->status['filters_missed'])==0){
            $this->scrawl->info("All filters found");
        } else {
            $this->scrawl->warn("Filters missed", implode(", ", $this->status['filters_missed']));
        }

    }
}